﻿using System;
using System.Buffers;
using System.Buffers.Binary;
using System.IO;
using System.Runtime.CompilerServices;

namespace ManagedTFRecordLibrary
{
    public sealed class TFRecordReader : IDisposable
    {
        private Stream _stream;
        private readonly long _initialPosition;
        private readonly bool _leaveOpen;

        public TFRecordReader(Stream stream, bool leaveOpen)
        {
            if (stream == null)
            {
                throw new ArgumentNullException(nameof(stream));
            }
            _stream = stream;
            _initialPosition = _stream.CanSeek ? _stream.Position : -1;
            _leaveOpen = leaveOpen;
        }

        private bool _isEof = false;
        private ulong? _dataLength = null;

        public bool TryReset()
        {
            if (_initialPosition == -1)
            {
                return false;
            }
            _stream.Seek(_initialPosition, SeekOrigin.Begin);
            _isEof = false;
            _dataLength = null;
            return true;
        }

        public bool TryGoForward()
        {
            CheckDisposed();
            if (_isEof)
            {
                return false;
            }
            if (_dataLength != null)
            {
                _stream.Seek((long)_dataLength.Value, SeekOrigin.Current);
                _stream.Seek(4, SeekOrigin.Current);
            }

            byte[] bytesBuffer = ArrayPool<byte>.Shared.Rent(12);
            Span<byte> buffer = bytesBuffer;

            try
            {
                if (_stream.Read(bytesBuffer, 0, 12) != 12)
                {
                    _isEof = true;
                    return false;
                }
                ulong length = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
                uint lengthCrc32 = UnmaskCrc32(BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(8, 4)));

                Crc32 crc32 = new Crc32(Crc32.CastagnoliPolynomial);
                if (crc32.Append(buffer.Slice(0, 8)).Value != lengthCrc32)
                {
                    _isEof = true;
                    return false;
                }

                _dataLength = length;
                return true;
            }
            finally
            {
                ArrayPool<byte>.Shared.Return(bytesBuffer);
            }
        }

        public long CurrentLength => (long)(_dataLength != null ? _dataLength.Value : 0);

        public bool TryReadCurrent(Span<byte> dest)
        {
            CheckDisposed();
            if (_isEof || _dataLength == null)
            {
                return false;
            }
            if ((ulong)dest.Length < _dataLength.Value)
            {
                return false;
            }
            int length = (int)_dataLength.Value;
            _dataLength = null;

            byte[] bytesBuffer = ArrayPool<byte>.Shared.Rent(81920);
            Span<byte> buffer = bytesBuffer;

            try
            {
                int copied = 0;
                int remaing = length;
                while (remaing >= 81920)
                {
                    if (_stream.Read(bytesBuffer, 0, 81920) != 81920)
                    {
                        return false;
                    }
                    buffer.Slice(0, 81920).CopyTo(dest.Slice(copied));
                    copied += 81920;
                    remaing -= 81920;
                }
                if (remaing > 0)
                {
                    if (_stream.Read(bytesBuffer, 0, remaing) != remaing)
                    {
                        return false;
                    }
                    buffer.Slice(0, remaing).CopyTo(dest.Slice(copied, remaing));
                }

                _stream.Read(bytesBuffer, 0, 4);
                uint expectedCrc32 = UnmaskCrc32(BinaryPrimitives.ReadUInt32LittleEndian(buffer.Slice(0, 4)));
                uint crc32 = (new Crc32(Crc32.CastagnoliPolynomial)).Append(dest.Slice(0, length)).Value;
                if (crc32 != expectedCrc32)
                {
                    return false;
                }
            }
            finally
            {
                ArrayPool<byte>.Shared.Return(bytesBuffer);
            }

            return true;
        }


        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static uint UnmaskCrc32(uint maskedCrc32)
        {
            unchecked
            {
                uint rot = maskedCrc32 - 0xa282ead8;
                return (rot >> 17) | (rot << 15);
            }
        }

        private void CheckDisposed()
        {
            if (_stream == null)
            {
                throw new ObjectDisposedException(nameof(TFRecordWriter));
            }
        }
        public void Dispose()
        {
            if (_stream != null && !_leaveOpen)
            {
                _stream.Dispose();
            }
            _stream = null;
        }
    }
}
